/*
 * Segue Audio Player
 * Copyright 2011 Fraser McCrossan frasermccrossan@gmail.com
 * 
 * This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published
 *  by the Free Software Foundation, either version 3 of the License,
 *  or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.

 */

package com.frasermccrossan.segue;

import java.io.IOException;

import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Binder;
import android.os.IBinder;
import android.os.PowerManager;
import android.preference.PreferenceManager;
import android.provider.BaseColumns;
import android.provider.MediaStore;
import android.provider.MediaStore.Audio.AudioColumns;
import android.widget.Toast;

import com.frasermccrossan.segue.PlayingScreen.PlayingScreenCallback;
import com.frasermccrossan.segue.providers.QueueProvider;

public class QueuePlayer extends Service
	implements MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener {

//	public static final String PLAY_POSITION = "play_position";
//	public static final String PLAY_DURATION = "play_duration";
	
	Toast mToast;
	
	Notification mNotification;
	
	// for the ListView passed in by the playing screen
	PlayingScreenCallback psCallback = null;
	
	MediaPlayer mPlayer = null;
	Uri currentUri = null;
	PowerManager.WakeLock mWakeLock = null;
	
	ContentResolver cr;
	PowerManager pm;

	public class QueueBinder extends Binder {
		QueuePlayer getService() {
			return(QueuePlayer.this);
		}
	}
	
	// this helper class computes some handy stats about the currently-playing track
	public class PlayProgress {
		
		public int duration; //milliseconds
		public int progress; //milliseconds
		public int remaining; // milliseconds
		public String elapsedText;
		public String remainingText;
		public double percentage_complete;
		boolean playing;
		
		PlayProgress(MediaPlayer player) {
			if (player != null && player.isPlaying()) {
				// if the player is actually running, fetch the current value
				init(player.getDuration(), player.getCurrentPosition());
				playing = true;
			}
			else {
				// otherwise, fetch the last saved value from the top of the queue - this really just allows
				// the playing screen code to be simpler and avoid showing 00:00 when stopped
				Cursor c = getFirstInQueue();
				if (c != null) {
					Cursor c2 = getDurationAndBookmark(c);
					c.close();
					if (c2 != null) {
						init(c2.getInt(0), c2.getInt(1));
						c.close();
					}
					else {
						init(0, 0);
					}
				}
				else {
					init(0, 0);
				}
			    playing = false;
			}
		}
		
		PlayProgress(int d, int p) {
			init(d, p);
			playing = true;
		}
		
		private void init(int d, int p) {
			duration = d; // without this, weird rounding errors on remainingText time
			progress = p;
			remaining = duration - progress;
			if (duration > 0) {
				percentage_complete = 100.0 * (double)progress / (double)duration;
				elapsedText = msToMinSec(progress);
				remainingText = msToMinSec(remaining);
			}
			else {
				duration = progress = 0;
				percentage_complete = 0.0;
				elapsedText = remainingText = msToMinSec(duration);
			}
		}
		
		private String msToMinSec(int msecs) {
			int seconds = msecs / 1000;
			int minutes = seconds / 60;
			seconds -= minutes * 60;
			return String.format("%02d:%02d", minutes, seconds);
		}
	}
	
	@Override
	public void onCreate() {
        mToast = Toast.makeText(getApplicationContext(), "", Toast.LENGTH_SHORT);
        cr = getContentResolver();
        pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
        mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "while playing");
        mWakeLock.setReferenceCounted(false);
        mNotification = new Notification(R.drawable.ic_stat_segue, getText(R.string.app_name), System.currentTimeMillis());
    }
	
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        mToast = Toast.makeText(QueuePlayer.this, "", Toast.LENGTH_SHORT);
        return START_STICKY;
    }

    
    public void createPlayer() {
    	if (mPlayer == null) {
    		mPlayer = new MediaPlayer();
    		mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
    		mPlayer.setOnCompletionListener(this);
    		mPlayer.setOnErrorListener(this);
    	}
    }
    
    public void destroyPlayer() {
    	if (mPlayer != null) {
    		mPlayer.reset();
    		mPlayer.release();
    		mPlayer = null;
    		currentUri = null;
    	}
    }
    
    public boolean isActive() {
    	return mPlayer != null;
    }
    
    public boolean isPlaying() {
    	return isActive() && mPlayer.isPlaying();
    }
    
    public PlayProgress getProgress() {
    	return new PlayProgress(mPlayer);
    }
    
    public void resetPlayer() {
    	createPlayer();
    	mPlayer.reset();
    	currentUri = null;
    }
    
    public void playToggle() {
    	if (isPlaying()) {
    		playStop(true);
    	}
    	else {
    		playStart();
    	}
    }
    
    public void playStart() {
    	resetPlayer();
    	playFirstInQueue();
    }
    
    // if not currently playing and preferences say to autostart on a track being added, start
    public void maybeAutoPlay() {
    	if (! isPlaying()) {
        	SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
        	if (prefs.getBoolean("autoplay", false)) {
        		playStart();
        	}
    	}
    }
    
    public void maybeSaveProgress(boolean saveProgress) {
    	if (isPlaying()) {
    		if (saveProgress) {
    			int pos = mPlayer.getCurrentPosition();
        		savePlayProgress(pos);
        		if (pos > 0) {
        			proclaim(R.string.saved_position);
        		}
    		}
    		else {
        		savePlayProgress(0);
    		}
    	}
    }
    
    public void playSkip(boolean saveForResume) {
    	maybeSaveProgress(saveForResume);
    	resetPlayer();
    	delTrack(QueueProvider.CURRENT_TRACK);
    	playFirstInQueue();
    }
	
	public void playLater(int fromPosition, boolean saveForResume) {
		maybeSaveProgress(saveForResume);
		resetPlayer();
		bumpTrack(fromPosition);
		playFirstInQueue();
	}
    
    public void playSkipBack(boolean saveForResume) {
    	maybeSaveProgress(saveForResume);
    	resetPlayer();
    	stepBackQueue();
    	playFirstInQueue();
    }

    public void playNext(int toPlayNext) {
    	cr.update(QueueProvider.CONTENT_URI, null, QueueProvider.MAGIC_PLAY_NEXT, new String[] { Integer.toString(toPlayNext) });
    }
    
    public void playStop(boolean saveForResume) {
    	maybeSaveProgress(saveForResume);
    	destroyPlayer();
    	stopForeground(true);
    	mWakeLock.release();
    }

    public void playPause() {
    	if (mPlayer.isPlaying()) {
        	mPlayer.pause();
    	}
    }
    
    public void playResume() {
    	if (mPlayer.isPlaying()) {
    		mPlayer.start();
    	}
    }
    
    // pass null to delete the position
    private void savePlayProgress(int position) {
    	if (currentUri != null) {
    		ContentValues cv = new ContentValues();
    		cv.put(MediaStore.Audio.Media.BOOKMARK, position);
    		cr.update(currentUri, cv, null, null);
    	}
    }

    // MediaPlayer listeners
    
    public void onCompletion(MediaPlayer mp) {
    	if (mp == mPlayer) {
    		savePlayProgress(0);
    		playSkip(false);
    	}
    }
    
    public boolean onError(MediaPlayer mp, int what, int extra){
    	if (mp == mPlayer) {
    		switch (what) {
    		case MediaPlayer.MEDIA_ERROR_SERVER_DIED:
    			playStop(false);
    			return true; // handled!
    		}
    	}
    	return false; // not handled, system will call onCompletion()
    }
    
    // QueueProvider stuff; basically all regular operations on the QueuePlayer are
    // funnelled through the QueuePlayer
    
    // fetch the highest value of the ORDER field
    private Integer getNextOrder() {
    	Cursor c;
    	Integer rOrder;
    	c = cr.query(QueueProvider.CONTENT_URI,
    				 new String[] { "count(*)" },
    				 QueueProvider.ORDER + ">=?",
    				 new String[] { QueueProvider.CURRENT_TRACK.toString() },
    				 null);
    	if (! c.moveToFirst()) {
    		return QueueProvider.CURRENT_TRACK;
    	}
    	rOrder = c.getInt(0);
    	c.close();
    	return rOrder;
    }
    
    public void addTrack(Track track) {
    	Integer nextPos = getNextOrder();
    	ContentValues insertVals = new ContentValues();
    	insertVals.put(QueueProvider.ORDER, nextPos);
    	insertVals.put(QueueProvider.CP_ID, track._id);
    	insertVals.put(AudioColumns.TITLE, track.title);
    	insertVals.put(AudioColumns.ARTIST, track.artist);
    	insertVals.put(AudioColumns.ALBUM, track.album);
    	insertVals.put(AudioColumns.TRACK, track.track);
    	insertVals.put(AudioColumns.DURATION, track.duration);
    	cr.insert(QueueProvider.CONTENT_URI, insertVals);
    	maybeAutoPlay();
    }

    public Track getRandomTrack() {
    	Track newTrack = null;
    	Cursor c = cr.query(QueueProvider.CONTENT_URI, 
    			new String[] { QueueProvider.CP_ID, AudioColumns.TITLE, AudioColumns.ARTIST,
    			AudioColumns.ALBUM, AudioColumns.TRACK, AudioColumns.DURATION },
    			QueueProvider.MAGIC_RANDOM_TRACK,
    			null, null);
    	if (c != null) {
    		if (c.moveToFirst()) {
    			newTrack = new Track(c.getLong(0), c.getString(1), c.getString(2), c.getString(3), c.getInt(4), c.getInt(5));
    		}
    		c.close();
    	}
    	return newTrack;
    }
    
    public void addPreviousTrack(int order) {
    	ContentValues cv = new ContentValues(1);
    	cv.put(QueueProvider.ORDER, 99999); // 99999 pretty much means "end"
    	cr.update(QueueProvider.CONTENT_URI, cv,
    			QueueProvider.ORDER + "=?", new String[] { Integer.toString(order) } );
    	maybeAutoPlay();
    }
    
	public void delTrack(Integer position) {
		cr.delete(QueueProvider.CONTENT_URI,
				  QueueProvider.ORDER + "=?",
				  new String[] { position.toString() });
	}

	public void stepBackQueue() {
		cr.update(QueueProvider.CONTENT_URI, null, QueueProvider.MAGIC_BACK_UP, null);
	}
	
	public void delByArtist(Integer position) {
		cr.delete(QueueProvider.CONTENT_URI,
				  String.format("%s = (select %s from %s where %s = ?) and %s > ?",
						  MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ARTIST, QueueProvider.QUEUE_TABLE, QueueProvider.ORDER, QueueProvider.ORDER),
				  new String[] { position.toString(), QueueProvider.CURRENT_TRACK.toString() });
	}

	public void delByAlbum(Integer position) {
		cr.delete(QueueProvider.CONTENT_URI,
				  String.format("%s = (select %s from %s where %s = ?) and %s > ?",
						  MediaStore.Audio.Media.ALBUM, MediaStore.Audio.Media.ALBUM, QueueProvider.QUEUE_TABLE, QueueProvider.ORDER, QueueProvider.ORDER),
						  new String[] { position.toString(), QueueProvider.CURRENT_TRACK.toString() });
	}
    
    public void clearQueue() {
		cr.delete(QueueProvider.CONTENT_URI,
				  QueueProvider.ORDER + ">?",
				  new String[] { QueueProvider.CURRENT_TRACK.toString() });
    	
    }
    
    public void shuffleQueue() {
    	String shuffleType;
    	SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
    	if (prefs.getString("shuffle", "shuffle_alternate_artist").equals("shuffle_alternate_artist")) {
    		shuffleType = QueueProvider.MAGIC_ARTIST_SHUFFLE;
    	}
    	else {
    		shuffleType = QueueProvider.MAGIC_RANDOM_SHUFFLE;
    	}
    	cr.update(QueueProvider.CONTENT_URI, null, shuffleType, null);
    }
    
    public void bumpTrack(int fromPosition) {
    	ContentValues cv = new ContentValues();
    	cv.put(QueueProvider.ORDER, fromPosition + 99999); // 99999 almost always means "play last"
    	cr.update(QueueProvider.CONTENT_URI,
    			cv,
    			QueueProvider.ORDER + "=?",
    			new String[] { Integer.toString(fromPosition) });
    }

	void verifyQueue() {
		Cursor c;
		c = cr.query(QueueProvider.CONTENT_URI,
			     new String[] { BaseColumns._ID,
								AudioColumns.TITLE, AudioColumns.ARTIST,
								AudioColumns.ALBUM, AudioColumns.DURATION,
								QueueProvider.CP_ID },
								null, null, null);
		if (c.moveToFirst()) {
			Uri trackUri = android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
			Cursor trackCursor;
			do {
				Integer id = c.getInt(0);
				String title = c.getString(1);
				String artist = c.getString(2);
				String album = c.getString(3);
				String duration = c.getString(4);
				Integer cp_id = c.getInt(5);
				// try to find this row in the actual media database
				// save the conditions here because we might need them in an update below if the ID changed
				trackCursor = cr.query(trackUri,
										new String[] { AudioColumns._ID },
										AudioColumns.TITLE + "=? and " + AudioColumns.ARTIST + "=? and " +
									    AudioColumns.ALBUM + "=? and " + AudioColumns.DURATION + " =?",
									    new String[] { title, artist, album, duration },
					  				    null);
				if (trackCursor != null) {
					if (trackCursor.moveToFirst()) {
						int new_id = trackCursor.getInt(0);
						if (cp_id != new_id) {
							// update with the new ID of this track
							ContentValues cv = new ContentValues();
							cv.put(QueueProvider.CP_ID, new_id);
							cr.update(QueueProvider.CONTENT_URI,
									  cv,
									  AudioColumns._ID + "=?", new String[] { id.toString() });
						}
						// else ID is the same, no action
					}
					else {
						// delete from queue, track no longer in media database
						cr.delete(QueueProvider.CONTENT_URI,
								  AudioColumns._ID + "=?",
								  new String[] { id.toString() });
					}
					trackCursor.close();
				}
			} while (c.moveToNext());
		}
	}
	
	Cursor getFirstInQueue () {
    	Cursor c;
    	c = cr.query(QueueProvider.CONTENT_URI,
				     new String[] { QueueProvider.CP_ID, MediaStore.Audio.Media.TITLE },
				     QueueProvider.ORDER + "=?",
				     new String[] { QueueProvider.CURRENT_TRACK.toString() },
				     null);
    	if (c == null) {
    		return c;
    	}
    	if (! c.moveToFirst()) {
    		c.close();
    		return null;
    	}
    	return c;
	}
	
	// given the cursor returned by getFirstInQueue, get another cursor with the
	// duration and bookmark from the media database
	Cursor getDurationAndBookmark(Cursor first) {
		Cursor c;
    	c = cr.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
			         new String[] { MediaStore.Audio.Media.DURATION, MediaStore.Audio.Media.BOOKMARK },
			         MediaStore.Audio.Media._ID + "=?",
			         new String[] { Integer.toString(first.getInt(0)) },
			         null);
		if (c == null) {
			return c;
		}
		if (! c.moveToFirst()) {
			c.close();
			return null;
		}
		return c;
	}
	
    void playFirstInQueue () {
    	Cursor c;
    	c = getFirstInQueue();
    	if (c == null) {
    		// possibly pick a new track here based on previously-played and preferences
    		playStop(false);
    	}
    	else {
    		mPlayer.reset();
    		int currentId = c.getInt(0);
    		String title = c.getString(1);
    		int bookmark = 0;
    		Cursor c2 = getDurationAndBookmark(c);
    		if (c2 != null) {
    			bookmark = c2.getInt(1);
    			c2.close();
    		}
    		currentUri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, currentId);
    		c.close();
    		try {
    			mPlayer.setDataSource(this, currentUri);
    			mPlayer.prepare();
    			mPlayer.seekTo(bookmark);
    			mPlayer.start();
    			if (bookmark > 0) {
    				proclaim(R.string.resumed);
    			}
//    			Cursor c2 = cr.query(QueueProvider.CONTENT_URI,
//    								 new String[] { "count(*)" },
//    								 QueueProvider.ORDER + ">?",
//    								 new String[] { QueueProvider.CURRENT_TRACK.toString() },
//    								 null);
    			int remaining = 0;
//    			if (c2 != null) {
//    				if (c2.moveToFirst()) {
//    					remainingText = c2.getInt(0);
//    				}
//    				c2.close();
//    			}
    			Intent notifIntent = new Intent(this, PlayingScreen.class);
    			PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notifIntent, 0);
    			if (remaining > 0) {
    				title += " " + String.format(getString(R.string.n_remaining), remaining);
    			}
    			mNotification.setLatestEventInfo(this, getText(R.string.playing),
    			        title, pendingIntent);
    			startForeground(1, mNotification);
    			mWakeLock.acquire();
    		}
    		catch (IOException e) {
    			// if we can't play something then it's probably missing
    			proclaim(getString(R.string.missing));
    			verifyQueue();
    			playSkip(false);
    		}
    	}
    }
    
    public void seekSeconds(int seconds) {
    	seekRelative(seconds * 1000);
    }
    
    public void seekPercent(int percent) {
    	int length = mPlayer.getDuration();
    	seekRelative(length * percent / 100);
    }
    
    public void seekRelative(int milliseconds) {
    	int position = mPlayer.getCurrentPosition();
    	int target = position + milliseconds;
    	if (target < 0) {
    		seekTo(0);
    	}
    	else {
        	if (target > mPlayer.getDuration()) {
        		seekTo(position + 1);
        	}
        	else {
        		seekTo(target);
        	}
    	}
    }
    
    public void seekTo(int position) {
    	if (isPlaying()) {
    		mPlayer.seekTo(position);
    	}
    }
    
	private final IBinder mBinder = new QueueBinder();
	
	@Override
	public IBinder onBind(Intent intent) {
		return mBinder;
	}

	@Override
	public void onDestroy() {
	    super.onDestroy();
	    destroyPlayer();
	}
	
    private void proclaim(int textId) {
    	proclaim(getString(textId));
    }
	
	public void proclaim(CharSequence text) {
		mToast.setText(text);
		mToast.show();
	}
}
